#include "PacmanEnvironment.h"
#include "Common.h"

#ifdef WIN32
	#include "freeglut/freeglut.h"
	#include <openal/al.h>
	#include <openal/alc.h>
#else
	#include <GL/freeglut.h>
	#include <AL/al.h>
	#include <AL/alc.h>
#endif

#include <vector>

#define CPU_CLOCK					3072000
#define FRAME_RATE					60
#define CYCLES_PER_FRAME			(CPU_CLOCK / FRAME_RATE)
#define HALF_CYCLES_PER_FRAME		(CYCLES_PER_FRAME / 4)
#define TIMESTEP_MILLISEC			(1000 / FRAME_RATE)

#define SOUND_CLOCK					(CPU_CLOCK / 32)
#define SOUND_RATE					(FRAME_RATE)
#define SAMPLE_RATE					44100 
//#define SAMPLES_PER_FRAME			((SAMPLE_RATE / FRAME_RATE) * 2)
#define SAMPLES_PER_FRAME			(SOUND_CLOCK / SOUND_RATE)

#define NUM_SOUND_BUFFERS				50

// glut forces us to use global vars :(
bool isPaused	= false;
bool doQuit		= false;

#pragma region Inputs Stuff

// IN0 and IN1
byte_t inputs[2];

enum Input0
{
	Input0_P1_JoystickUp		= 1,
	Input0_P1_JoystickLeft		= 2,
	Input0_P1_JoystickRight		= 4,
	Input0_P1_JoystickDown		= 8,
	Input0_RackAdvance			= 16,
	Input0_CoinSlot1			= 32,
	Input0_CoinSlot2			= 64,
	Input0_CreditButton			= 128,
};

enum Input1
{
	Input1_P2_JoystickUp		= 1, // Player 2 inputs are available only in Cocktail cabinet mode
	Input1_P2_JoystickLeft		= 2,
	Input1_P2_JoystickRight		= 4,
	Input1_P2_JoystickDown		= 8,
	Input1_BoardTest			= 16,
	Input1_P1_StarButton		= 32,
	Input1_P2_StarButton		= 64,
	Input1_CabinetMode_Table	= 0,
	Input1_CabinetMode_Upright	= 128,
};

void specialKeyDown(int key, int x, int y)
{
	IGNORE_PARAM(x); IGNORE_PARAM(y);

	switch(key)
	{
		case GLUT_KEY_UP:	inputs[0] &= ~(static_cast<byte_t>(Input0_P1_JoystickUp));		break;
		case GLUT_KEY_LEFT:	inputs[0] &= ~(static_cast<byte_t>(Input0_P1_JoystickLeft));	break;
		case GLUT_KEY_RIGHT:inputs[0] &= ~(static_cast<byte_t>(Input0_P1_JoystickRight));	break;
		case GLUT_KEY_DOWN:	inputs[0] &= ~(static_cast<byte_t>(Input0_P1_JoystickDown));	break;
		default: break;
	}
}

void specialKeyUp(int key, int x, int y)
{
	IGNORE_PARAM(x); IGNORE_PARAM(y);

	switch(key)
	{
		case GLUT_KEY_UP:	inputs[0] |= static_cast<byte_t>(Input0_P1_JoystickUp);	break;
		case GLUT_KEY_LEFT:	inputs[0] |= static_cast<byte_t>(Input0_P1_JoystickLeft);	break;
		case GLUT_KEY_RIGHT:inputs[0] |= static_cast<byte_t>(Input0_P1_JoystickRight);	break;
		case GLUT_KEY_DOWN:	inputs[0] |= static_cast<byte_t>(Input0_P1_JoystickDown);	break;
		default: break;
	}
}

void keyDown(unsigned char key, int x, int y)
{
	IGNORE_PARAM(x); IGNORE_PARAM(y);

	switch(key)
	{
	case 'q': doQuit = true;														break;
	case 't': inputs[1] &= ~(static_cast<byte_t>(Input1_BoardTest));				break;
	case 'k': inputs[0] &= ~(static_cast<byte_t>(Input0_RackAdvance));				break;
	case '5': inputs[0] &= ~(static_cast<byte_t>(Input0_CoinSlot1));				break;
	case '6': inputs[0] &= ~(static_cast<byte_t>(Input0_CoinSlot2));				break;
	case '1': inputs[1] &= ~(static_cast<byte_t>(Input1_P1_StarButton));			break;
	case '2': inputs[1] &= ~(static_cast<byte_t>(Input1_P2_StarButton));			break;

	default: break;
	}
}

void keyUp(unsigned char key, int x, int y)
{
	IGNORE_PARAM(x); IGNORE_PARAM(y);

	switch(key)
	{
	case 'p': isPaused = !isPaused;													break;
	case 'k': inputs[0] |= static_cast<byte_t>(Input0_RackAdvance);					break;
	case 't': inputs[1] |= static_cast<byte_t>(Input1_BoardTest);					break;
	case '5': inputs[0] |= static_cast<byte_t>(Input0_CoinSlot1);					break;
	case '6': inputs[0] |= static_cast<byte_t>(Input0_CoinSlot2);					break;
	case '1': inputs[1] |= static_cast<byte_t>(Input1_P1_StarButton);				break;
	case '2': inputs[1] |= static_cast<byte_t>(Input1_P2_StarButton);				break;

	default: break;
	}
}

#pragma endregion

#pragma region Drawing Stuff

GLuint PrepareTexture()
{
	GLuint textureId;

	glGenTextures(1, &textureId);
	glBindTexture(GL_TEXTURE_2D, textureId);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 224, 288, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
	glBindTexture(GL_TEXTURE_2D, 0);

	return textureId;
}

void DrawTexture(GLuint textureId, const byte_t* textureBuffer)
{
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, textureId);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 224, 288, 0, GL_RGB, GL_UNSIGNED_BYTE, textureBuffer);

	glBegin(GL_QUADS);
		glTexCoord2d(0.0f,0.0f); glVertex2d(-1.0f,-1.0f);
		glTexCoord2d(1.0f,0.0f); glVertex2d( 1.0f,-1.0f);
		glTexCoord2d(1.0f,1.0f); glVertex2d( 1.0f, 1.0f);
		glTexCoord2d(0.0f,1.0f); glVertex2d(-1.0f, 1.0f);
	glEnd();

	glBindTexture(GL_TEXTURE_2D, 0);
}

void resize(int width, int heigth)
{
	glViewport(0, 0, width, heigth);
	glutReshapeWindow(width, heigth);
	glutPostRedisplay();
}

void dummyDisplay(){}

#pragma endregion

#pragma region Sound Stuff

const char * defname; 
ALCdevice * device;
ALCcontext * context;

void checkError()
{
	ALenum err = alGetError();
	if (err != AL_NO_ERROR) printf("%04X\n", err);
}

void initSound()
{
	defname = alcGetString(0, ALC_DEFAULT_DEVICE_SPECIFIER);
	checkError();

	device = alcOpenDevice(defname);
	checkError();

	context = alcCreateContext(device, 0);
	checkError();

	alcMakeContextCurrent(context);
	checkError();
}

void releaseSound()
{
	alcMakeContextCurrent(0);
	checkError();

	alcDestroyContext(context);
	checkError();

	alcCloseDevice(device);
	checkError();
}



#pragma endregion

int main(int argc, char **argv)
{
	int width = VideoHardware::video_width * 2;
	int height = VideoHardware::video_height * 2; 
	float aspectRatio = (float)width / (float)height;	

	// init GLUT and create Window
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
	glutInitWindowPosition(100,100);
	glutInitWindowSize(width, height);
	glutCreateWindow("Pacman");

	GLuint textureId = PrepareTexture();

	glutKeyboardFunc(keyDown);
	glutKeyboardUpFunc(keyUp);

	glutSpecialFunc(specialKeyDown);
	glutSpecialUpFunc(specialKeyUp);
	
	//'dummyDisplay' is empty but without it,
	//GLUT don't call 'fghReshapeWindow' after
	//user window resize .... GLUT SUCKS	
	glutDisplayFunc(dummyDisplay);

	PacmanEnvironment env;
	const VideoHardware * videoHw = env.GetVideoHW();
	const SoundHardware * soundHw = env.GetSoundHW();

	env.SetDipSwitchSettings(CoinsPerGame_1Coin | NumberOfLives_3Lives | BonusScore_15000 | Difficulty_Normal | GhostNames_Normal);

	// Input 0 is all cleared (all ones)
	inputs[0] = 0xff;

	// Input 1 is all cleared, except for cabinet mode (table or upright)
	// Cabinet mode does not affect 1 player game
	inputs[1] = 0x7F | Input1_CabinetMode_Table;

	int elapsed = 0;
	int lastUpdate = glutGet(GLUT_ELAPSED_TIME);
	
	initSound();

	ALuint soundSource, soundBuffers[NUM_SOUND_BUFFERS];
	alGenSources(1, &soundSource);
	checkError();

	alGenBuffers(NUM_SOUND_BUFFERS, soundBuffers);
	checkError();
	
	// Sound buffer
	sword_t samples[SAMPLES_PER_FRAME];

	// Used to skip sound frames
	size_t frameCounter = 0;

	// Number of queued buffer. Never more than 'x'... little trick to keep audio synched
	size_t queuedBuffers = 0;

	std::vector<ALuint> freeBuffers (NUM_SOUND_BUFFERS);
	for (size_t i = 0; i < NUM_SOUND_BUFFERS; ++i) freeBuffers.push_back(soundBuffers[i]);
	
	while(env.IsRunning() && !doQuit)
	{
		elapsed = glutGet(GLUT_ELAPSED_TIME) - lastUpdate;

		int newWidth = glutGet(GLUT_WINDOW_WIDTH);
		int newHeight = glutGet(GLUT_WINDOW_HEIGHT);

		if(newWidth != width || newHeight != height)
		{
			height = newHeight;
			width = static_cast<int>(aspectRatio * height); // keep the right pacman aspect-ratio
			resize(width, height);
		}

		// events handle
		glutMainLoopEvent();

		// inject player inputs
		env.SetInput(inputs);

		// update
		if (!isPaused) env.Update(CYCLES_PER_FRAME);
		
		// draw
		DrawTexture(textureId, videoHw->GetVideoMemory());

		// swap
		glutSwapBuffers();

		if (!isPaused)
		{

			// free processed buffers
			ALint processedBuffer;
			alGetSourcei(soundSource, AL_BUFFERS_PROCESSED, &processedBuffer);

			while (processedBuffer-- > 0)
			{
				ALuint toFree;
				alSourceUnqueueBuffers(soundSource, 1, &toFree);
				freeBuffers.push_back(toFree);
				queuedBuffers--;
			}

			if (soundHw->IsEnabled() && queuedBuffers <= 2) // this should keep the audio sync
			{
				env.Play(samples, SAMPLES_PER_FRAME, SAMPLE_RATE);

				// find a free buffer
				assert(freeBuffers.size() > 0 && "The are no free sound buffers!");
				ALuint newBuffer = freeBuffers.back();
				freeBuffers.pop_back();
					
				alBufferData(newBuffer, AL_FORMAT_MONO16, samples, SAMPLES_PER_FRAME, SAMPLE_RATE);
				checkError();

				alSourceQueueBuffers(soundSource, 1, &newBuffer);
				checkError();

				queuedBuffers++;
				
				ALint state;
				alGetSourcei(soundSource, AL_SOURCE_STATE, &state);

				if (state != AL_PLAYING && frameCounter > 2)
				{
					frameCounter = 0;
					alSourcef(soundSource, AL_GAIN, soundHw->IsEnabled() ? 1.0f : 0.0f);
					checkError();

					alSourcePlay(soundSource);
					checkError();
				}
			}
		}
		
		++frameCounter;

		// sleep
		while (glutGet(GLUT_ELAPSED_TIME) - lastUpdate < TIMESTEP_MILLISEC);
		lastUpdate = glutGet(GLUT_ELAPSED_TIME);
	}

	glutLeaveMainLoop();

	alDeleteBuffers(NUM_SOUND_BUFFERS, soundBuffers);
	alDeleteSources(1, &soundSource);
	releaseSound();

	return 0;
}
